﻿using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.Linq;

namespace Sparklings.JsonUtils
{
    public class JsonOverrider
    {
        /// <summary>
        /// Contains the deleguates to create the objects and the deleguates to write them in a json form.
        /// 
        /// All the deleguates should be non-null
        /// </summary>
        private Dictionary<Type, Tuple<OverridingJsonConverter.ConvertFromJson, OverridingJsonConverter.ConvertToJson>> ConvertMethods
            = new Dictionary<Type, Tuple<OverridingJsonConverter.ConvertFromJson, OverridingJsonConverter.ConvertToJson>>();
        private OverridingJsonConverter parent;
        public Stack<object> ParentItemsStack { get; private set; }
        internal JsonOverrider(OverridingJsonConverter parent,object parentInStack)
        {
            this.parent = parent;
            ParentItemsStack = new Stack<object>();
            if (parentInStack!=null)
            {
                ParentItemsStack.Push(parentInStack);
            }
        }
        public void addToConvertMethods(Type type, OverridingJsonConverter.ConvertFromJson from, OverridingJsonConverter.ConvertToJson to)
        {
            #region null args
            if (from == null)
            {
                throw new NullReferenceException("Parameter \"from\" cannot be null.");
            }
            if (to == null)
            {
                throw new NullReferenceException("Parameter \"to\" cannot be null.");
            }
            #endregion
            ConvertMethods[type] = new Tuple<OverridingJsonConverter.ConvertFromJson, OverridingJsonConverter.ConvertToJson>(from, to);
        }
        #region Reading
        /// <summary>
        /// Transforms the given <see cref="JToken"/> in a instance of an object
        /// of type <typeparamref name="T"/>.
        /// </summary>
        /// <remarks>
        /// Should be called instead of casting the <see cref="JToken"/> elements,
        /// to allow the customisation of reading to work on subclasses.
        /// </remarks>
        /// <typeparam name="T">The type of object to return, used to interpretate
        /// the <paramref name="toRead"/></typeparam>
        /// <param name="toRead">The <see cref="JToken"/> to convert to an
        /// object of the given type.</param>
        /// <param name="me">The instance calling the function. This is needed
        /// when a sub-class references an parent instance.</param>
        /// <returns></returns>
        public T Read<T>(JToken toRead, object me = null)
        {
            return (T)Read(typeof(T), toRead, me);
        }
        /// <summary>
        /// Transforms the given <c>JToken</c> in a instance of an object
        /// of type <paramref name="t"/>.
        /// </summary>
        /// <remarks>
        /// Should be called instead of casting the <see cref="JToken"/> elements,
        /// to allow the customisation of reading to work on subclasses.
        /// </remarks>
        /// <param name="t">The type of object to return, used to interpretate
        /// the <paramref name="toRead"/></param>
        /// <param name="toRead">The <see cref="JToken"/> to convert to an
        /// object of the given type.</param>
        /// <param name="me">The instance calling the function. This is needed
        /// when a sub-class references an parent instance.</param>
        /// <returns>An object of type <paramref name="t"/> decoded from
        /// the Json.</returns>
        public object Read(Type t, JToken toRead, object me = null)
        {
            #region null parameters
            if (t == null)
            {
                throw new ArgumentNullException("t");
            }
            if (toRead == null)
            {
                throw new ArgumentNullException("toConvert");
            }
            #endregion
            if (me != null) { ParentItemsStack.Push(me); }
            object ret;
            if (!parent.CanConvert(t))
            {
                ret = toRead.GetType().GetMethod("ToObject", new Type[] { typeof(JsonSerializer) })
                    .MakeGenericMethod(t).Invoke(toRead, new object[] { parent.serializer });
            }
            else
            {
                ret = ConvertMethods[t].Item1(this, toRead);
                if (ret != null && !t.IsAssignableFrom(ret.GetType()))
                {
                    throw new InvalidCastException("Can't cast type " + ret.GetType().FullName + " to " + t.FullName + ".");
                }
            }
            if (me != null) { ParentItemsStack.Pop(); }
            return ret;
        }
        #endregion
        #region Writing
        /// <summary>
        /// Writes the given object to a <see cref="JToken"/>, using customized writing
        /// if needed.
        /// </summary>
        /// <remarks>
        /// If no method is defined for the exact type, but one exist for one of
        /// its superclasses.
        /// </remarks>
        /// <param name="toWrite">The object to write.</param>
        /// <returns>A <see cref="JToken"/> in which <paramref name="toWrite"/>
        /// is serialized.</returns>
        public JToken Write(object toWrite)
        {
            // Optimize : check a given type only once.
            if (ConvertMethods.ContainsKey(toWrite.GetType()))
            {
                // Easy : No inheritance.
                return ConvertMethods[toWrite.GetType()].Item2(this, toWrite) ?? new JValue((object)null);
            }
            else
            {
                // Hard : the type is not known, but maybe it is a subclass of a
                // known type.
                foreach (var kv in ConvertMethods)
                {
                    Type type = toWrite.GetType();
                    if (type.IsSubclassOf(kv.Key))
                    {
                        return kv.Value.Item2(this, toWrite) ?? new JValue((object)null);
                    }
                }
                // No, we did all this for nothing, going standard writing.
                if (toWrite == null)
                {
                    return null;
                }
                else
                {
                    return toWrite != null ? JToken.FromObject(toWrite, parent.serializer) : new JValue((object)null);
                }
            }
        }
        #endregion
        /// <summary>
        /// Attempts to find the first parent of the currently disassembled object
        /// that has <typeparamref name="T"/> as a type.
        /// </summary>
        /// <remarks>
        /// Unfortunately, if the parent was default-read (or wrote), it wont be
        /// found. Given that the default parsing is not in our hands, this probably
        /// won't be solved, unless an hack is found, or part of the default parsing
        /// is rewroten.
        /// </remarks>
        /// <typeparam name="T">The type of the parent required.</typeparam>
        /// <returns>The parent, or null if it can't find any.</returns>
        public T GetClosestParentWithType<T>()
        {
            return (T)ParentItemsStack.LastOrDefault(obj => typeof(T).IsAssignableFrom(obj.GetType()));
        }
        /// <summary>
        /// Returns wether the type needs to be read in a custom way.
        /// </summary>
        /// <param name="objectType">The type to check.</param>
        /// <returns><c>true</c> if the type should be disassembled in a custom way.</returns>
        public bool CanConvert(Type objectType)
        {
            foreach (var kv in ConvertMethods)
            {
                if (objectType.IsSubclassOf(kv.Key))
                {
                    return true;
                }
            }
            return ConvertMethods.ContainsKey(objectType);
        }
    }
    public class OverridingJsonConverter : JsonConverter
    {
        
        public OverridingJsonConverter(object parent = null)
        {
            serializer = new JsonSerializer { Converters = { this, new StringEnumConverter() } };
            jsonOverrider = new JsonOverrider(this,parent);
        }
        private JsonOverrider jsonOverrider;
        public delegate object ConvertFromJson(JsonOverrider converter, JToken toConvert);
        public delegate JToken ConvertToJson(JsonOverrider converter, object value);
        internal JsonSerializer serializer;

        /// <summary>
        /// Add a type to be handled in a special way by the converter.
        /// </summary>
        /// <param name="type">The type to handle in a non-default way.</param>
        /// <param name="from">A method performing the conversion from a json token (usually 
        /// an object, but this could also be a list, or any valid json token) to an instance
        /// of type <paramref name="type"/>. It will be called each time the type is 
        /// encountered, either explicitly via <c>ReadObject</c> or encountered in a
        /// default-converted type.</param>
        /// <param name="to">A method performing the conversion to a json token (usually 
        /// an object, but this could also be a list, or any valid json token) from an instance
        /// of type <paramref name="type"/>. It will be called each time the type is 
        /// encountered, either explicitly via <c>WriteObject</c> or encountered in a
        /// default-converted type.</param>
        public void addToConvertMethods(Type type, ConvertFromJson from, ConvertToJson to)
        {
            jsonOverrider.addToConvertMethods(type, from, to);
        }

        public override bool CanConvert(Type objectType)
        {
            return jsonOverrider.CanConvert(objectType);
        }

        public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
        {
            if (!CanConvert(objectType))
            {
                throw new NotImplementedException();
            }
            else
            {
                return jsonOverrider.Read(objectType, JToken.ReadFrom(reader));
            }
        }

        public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
        {
            if (!CanConvert(value.GetType()))
            {
                throw new NotImplementedException();
            }
            else
            {
                jsonOverrider.Write(value).WriteTo(writer);
            }
        }
    }
}
